package de.bht.esa.weather.googlegeocodingclient;

import com.sun.jersey.api.client.Client;
import com.sun.jersey.api.client.WebResource;
import com.sun.jersey.api.client.config.ClientConfig;
import com.sun.jersey.api.client.config.DefaultClientConfig;
import de.bht.esa.weather.model.Location;
import de.bht.esa.weather.model.geo.GeoBounds;
import de.bht.esa.weather.model.geo.GeoCoordinates;
import java.util.logging.Logger;
import javax.ws.rs.core.MediaType;
import org.codehaus.jettison.json.JSONArray;
import org.codehaus.jettison.json.JSONException;
import org.codehaus.jettison.json.JSONObject;

/**
 * Konsumiert den Google-Geocoding-Service via REST
 * 
 * @author Andreas Goese
 * @author Sven Höche
 */
public class GoogleGeocodingClient {
    private final static Logger LOG = Logger.getLogger(GoogleGeocodingClient.class.getName());
    
    public enum GoogleGeocodingResponseStatus {
        OK("OK"),
        ZERO_RESULTS("ZERO_RESULTS"),
        OVER_QUERY_LIMIT("OVER_QUERY_LIMIT"),
        REQUEST_DENIED("REQUEST_DENIED"),
        INVALID_REQUEST("INVALID_REQUEST");
        
        private final String statusValue;
        
        private GoogleGeocodingResponseStatus(final String s){ 
            statusValue = s; 
        }
        
        public String toString() { 
            return statusValue; 
        }
    }
    
    private WebResource webResource;
    private Client client;
    private static final String BASE_URI = "http://maps.googleapis.com/maps/api/geocode/json";

    public GoogleGeocodingClient() {
        ClientConfig config = new DefaultClientConfig();
        client = Client.create(config);
        webResource = client.resource(BASE_URI).queryParam("language", "DE");
    }

    /**
     * Fragt bei Google die Koordinaten für einen beliebigen Adress-String an
     * 
     * @param addressQuery Die Adresse für die die Koordinaten gesucht werden sollen
     * @return Die Location mit den gewünschten Koordinaten.
     * @throws JSONException 
     */
    public Location geocode(String addressQuery, boolean sensor) throws JSONException{
        if(addressQuery == null || addressQuery.equals("")){
            throw new IllegalArgumentException(this.getClass().getName() + "::geocode: Missing or empty Argument: adressQuery");
        }
        
        String resultString = this.webResource.queryParam("address", addressQuery).queryParam("sensor",Boolean.toString(sensor)).accept(MediaType.APPLICATION_JSON).get(String.class);
        JSONObject resultObject = new JSONObject(resultString);
        
        if(!resultObject.getString("status").equalsIgnoreCase(GoogleGeocodingResponseStatus.OK.toString())){
            throw new IllegalStateException(resultObject.getString("status"));
        }
        
        return this.assambleLocation(this.getBestResultPart(resultObject.getJSONArray("results")));
    }
    
    /**
     * Führt eine Rückwärtssuche durch, bei der der passende Ort für die angegebenen Koordniaten gesucht werden soll.
     * 
     * @param coordinates Die Koordinaten
     * @param sensor Gibt an, ob die Korrdinaten durch einen GPS-Sensor ermittelt wurden.
     * @return Die Location die den angefragten Ort enthält
     * @throws JSONException 
     */
    public Location reverseGeocode(GeoCoordinates coordinates, boolean sensor) throws JSONException{        
        String resultString = this.webResource.queryParam("latlng", coordinates.toString()).queryParam("sensor",Boolean.toString(sensor)).accept(MediaType.APPLICATION_JSON).get(String.class);
        JSONObject resultObject = new JSONObject(resultString);
        
        if(!resultObject.getString("status").equalsIgnoreCase(GoogleGeocodingResponseStatus.OK.toString())){
            throw new IllegalStateException(resultObject.getString("status"));
        }
                
        return this.assambleLocation(this.getBestResultPart(resultObject.getJSONArray("results")));
     }
    
    
    private Location assambleLocation(JSONObject resultPart) throws JSONException{
        Location tmpLocation = new Location();
        
        JSONArray addressComponents = resultPart.getJSONArray("address_components");
        tmpLocation.setAddress(this.getAddressComponentsLongName("route", addressComponents) + " " + this.getAddressComponentsLongName("street_number", addressComponents));
        tmpLocation.setBounds(new GeoBounds(
                new GeoCoordinates(resultPart.getJSONObject("geometry").getJSONObject("viewport").getJSONObject("southwest").getDouble("lat"), resultPart.getJSONObject("geometry").getJSONObject("viewport").getJSONObject("southwest").getDouble("lng")), //SW
                new GeoCoordinates(resultPart.getJSONObject("geometry").getJSONObject("viewport").getJSONObject("northeast").getDouble("lat"), resultPart.getJSONObject("geometry").getJSONObject("viewport").getJSONObject("northeast").getDouble("lng"))  //NE
        ));
        tmpLocation.setCenter(new GeoCoordinates(resultPart.getJSONObject("geometry").getJSONObject("location").getDouble("lat"), resultPart.getJSONObject("geometry").getJSONObject("location").getDouble("lng")));
        tmpLocation.setCountry(this.getAddressComponentsLongName("country", addressComponents));
        tmpLocation.setCounty(this.getAddressComponentsLongName("administrative_area_level_3", addressComponents));
        tmpLocation.setDistrict(this.getAddressComponentsLongName("sublocality", addressComponents));
        tmpLocation.setName(this.getAddressComponentsLongName("locality", addressComponents));
        tmpLocation.setState(this.getAddressComponentsLongName("administrative_area_level_1", addressComponents));
        tmpLocation.setZip(this.getAddressComponentsLongName("postal_code", addressComponents));
        
        return tmpLocation;
    }
    
    
    private String getAddressComponentsLongName(String type, JSONArray addressComponents) throws JSONException{
        for(int i = 0; i < addressComponents.length(); i++){
            JSONObject tmpAddressComponent = addressComponents.getJSONObject(i);
            if(this.containsType(type,tmpAddressComponent.getJSONArray("types"))){
                return tmpAddressComponent.getString("long_name");
            }
        }
        return null;
    }
    
    private boolean containsType(String type, JSONArray typesArray) throws JSONException{
        for(int i = 0; i < typesArray.length(); i++){
            if(typesArray.getString(i).equalsIgnoreCase(type)){
                return true;
            }
        }
        return false;
    }
    
    
    
    //<-- NUR FÜR REVERSE-GEOCODING AB HIER
    
    private JSONObject getBestResultPart(JSONArray resultsArray) throws JSONException{        
        String[] bestLocationTypes = {"street_address","postal_code","locality","neighborhood","administrative_area_level_3","administrative_area_level_2","administrative_area_level_1","country"};
        for(int i = 0; i < bestLocationTypes.length; i++){
            JSONObject bestLocationResult = this.extractLocationResultPart(bestLocationTypes[i], resultsArray);
            if(bestLocationResult != null){
                return bestLocationResult;
            }
        }
        return null;
    }
    
    private JSONObject extractLocationResultPart(String type, JSONArray resultsArray) throws JSONException{
        for(int i = 0; i < resultsArray.length(); i++){
            if(this.containsType(type, resultsArray.getJSONObject(i).getJSONArray("types"))){
                return resultsArray.getJSONObject(i);
            }
        }
        return null;
    }
    

    
    //NUR FÜR REVERSE-GEOCODING BIS HIER -->

    public void close() {
        client.destroy();
    }
    
}
